package com.nageoffer.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.project.common.convention.abstractException.ClientException;
import com.nageoffer.shortlink.project.common.convention.abstractException.ServiceException;
import com.nageoffer.shortlink.project.common.enums.VailDateTypeEnum;
import com.nageoffer.shortlink.project.dao.entity.*;
import com.nageoffer.shortlink.project.dao.mapper.*;
import com.nageoffer.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.project.dto.resp.*;
import com.nageoffer.shortlink.project.service.ShortLinkService;
import com.nageoffer.shortlink.project.toolkit.HashUtil;
import com.nageoffer.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.*;
import static com.nageoffer.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 * 短链接接口实现层
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    @Value("${short-link.stats.locale.amap-key}")
    private String amapKey;
    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;


    @Override
    public void redirect(String shortUri, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException {
        /*
        缓存穿透（Cache Miss）：
        -存穿透指的是针对某个不存在于缓存中的数据进行访问请求时，由于缓存无法命中，导致继续向后端数据存储进行查询。如果恶意请求频繁地查询不存在于缓存中的数据，
        就可能导致频繁地访问后端存储，造成查询压力过大，甚至会对后端存储造成严重影响。
        典型的解决办法是使用布隆过滤器（Bloom Filter）等机制，在缓存层拦截掉那些明显无效的请求，将不存在于缓存中的数据直接丢弃，从而避免对后端存储的频繁请求。

        缓存击穿（Cache Breakdown）：
        缓存击穿指的是当针对一个热点数据进行并发请求时，当某一时刻缓存中该热点数据失效后，量的并发请求同时涌入后端存储进行查询，造成瞬间的压力激增，可能会导致后端存储的宕机等问题。
        典型的解决办法是在缓存失效后，将第一个请求加载的数据加入到缓存中，而其他并发请求在缓存失效期间等待，以避免同时涌入后端存储。

        缓存雪崩
        缓存雪崩是指在某个时间点上，缓存中的大量数据集中过期失效，导致大量请求直接落在了底层数据库或其他数据存储系统上，造成系统负载剧增、响应时间长甚至系统崩溃的现象。
        典型的解决办法是设置不同的过期时间：避免让大量缓存数据在同一时间点上过期失效，可以给缓存数据设置随机的过期时间或者在原有的过期时间上加上一个随机的时间偏移量。
        使用热点数据预加载：对于热点数据，可以通过定时任务等方式提前加载到缓存中，避免在缓存失效时由大量请求查询。
        使用多级缓存：设置多级缓存（如本地缓存 + 分布式缓存），可以提高系统对缓存雪崩的容错能力，即使分布式缓存层出现问题，也可以从本地缓存中获取部分数据。
        限流和降级：对于热点数据的查询请求可以进行限流处理，避免同时涌入大量查询请求。另外，针对缓存失效时的请求，可以采取降级策略，返回默认值或兜底数据，避免对底层数据存储系统的压力过大。
        */
        //拼接完整短链接
        //获取请求的协议 + :// + 域名 + 端口号 / + 短链接 httpServletRequest.getScheme() + "://" +
        String serverPort = Optional.of(httpServletRequest.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        String fullShortUrl = httpServletRequest.getServerName() + serverPort + "/" + shortUri;
        // 缓存击穿：从缓存获取原始链接G
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 获取到了直接重定向
        if (StrUtil.isNotBlank(originalLink)) {
            shortLinkStats(fullShortUrl, null, httpServletRequest, httpServletResponse);
            httpServletResponse.sendRedirect(originalLink);
            return;
        }
        // 缓存穿透：查询布隆过滤器
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        // 不存在直接返回
        if (!contains) {
            httpServletResponse.sendRedirect("/page/notfound");
            return;
        }
        // 缓存穿透(布隆误判情况下)：判断缓存是否有短链接的空值跳转
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        // 如果有短链接的空值跳转直接返回
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            httpServletResponse.sendRedirect("/page/notfound");
            return;
        }
        // 缓存击穿：加锁
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 获取锁
        lock.lock();
        try {
            // 缓存击穿：再次从缓存获取原始链接
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originalLink)) {
                // 如果不为空直接重定向
                shortLinkStats(fullShortUrl, null, httpServletRequest, httpServletResponse);
                httpServletResponse.sendRedirect(originalLink);
                return;
            }
            //查询路由表的完整短链接是否存在
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl));
            //不存在就直接返回不管了
            if (BeanUtil.isEmpty(shortLinkGotoDO)) {
                // 缓存穿透(布隆误判情况下)：设置缓存短链接的空值跳转key
                stringRedisTemplate.opsForValue().setIfAbsent(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30L, TimeUnit.MINUTES);
                httpServletResponse.sendRedirect("/page/notfound");
                return;
            }
            //查询原始链接
            ShortLinkDO shortLinkDO = lambdaQuery().eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .one();
            // 原始链接不存在或过期
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && LocalDateTime.now().isAfter(shortLinkDO.getValidDate()))) {
                stringRedisTemplate.opsForValue().setIfAbsent(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30L, TimeUnit.MINUTES);
                httpServletResponse.sendRedirect("/page/notfound");
                return;
            }
            // 缓存击穿：设置缓存
            stringRedisTemplate.opsForValue().setIfAbsent(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),
                    TimeUnit.MILLISECONDS
            );
            shortLinkStats(fullShortUrl, shortLinkGotoDO.getGid(), httpServletRequest, httpServletResponse);
            httpServletResponse.sendRedirect(shortLinkDO.getOriginUrl());

        } finally {
            lock.unlock();
        }
    }

    /**
     * 统计短链接的访问量和用户唯一访问量（UV、PV）。
     *
     * @param fullShortUrl        完整的短链接URL
     * @param gid                 短链接的分组ID，如果为空则从数据库查询
     * @param httpServletRequest  HTTP请求对象，用于获取请求中的Cookie
     * @param httpServletResponse HTTP响应对象，用于设置响应中的Cookie
     */
    private void shortLinkStats(String fullShortUrl, String gid, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        // 用于标记是否是用户第一次访问
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 从请求中获取所有的Cookie
        Cookie[] cookies = httpServletRequest.getCookies();
        try {
            AtomicReference<String> uv = new AtomicReference<>();
            // 创建一个任务，用于向响应中添加一个用户唯一标识的Cookie
            Runnable addResponseCookieTask = () -> {
                // 生成一个UUID作为用户唯一标识
                uv.set(UUID.fastUUID().toString());
                // 创建并设置Cookie
                Cookie uvCookie = new Cookie("uv", uv.get());
                uvCookie.setMaxAge(60 * 60 * 24 * 30); // 设置Cookie的过期时间为30天
                uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length())); // 设置Cookie的作用路径
                httpServletResponse.addCookie(uvCookie);
                // 标记为用户第一次访问
                uvFirstFlag.set(Boolean.TRUE);
                // 将用户唯一标识添加到Redis的HyperLogLog中，用于统计UV
                stringRedisTemplate.opsForHyperLogLog().add(String.format(STATS_UV_SHORT_LINK_KEY, fullShortUrl) + uv.get());
            };
            // 如果请求中存在Cookie，则尝试更新或添加用户唯一标识的Cookie
            if (ArrayUtil.isNotEmpty(cookies)) {
                Arrays.stream(cookies)
                        .filter(each -> Objects.equals(each.getName(), "uv"))
                        .findFirst()
                        .map(Cookie::getValue)
                        .ifPresentOrElse(each -> {
                            uv.set(each);
                            // 如果已存在用户唯一标识，则更新Redis中的HyperLogLog
                            Long uvadded = stringRedisTemplate.opsForHyperLogLog().add(String.format(STATS_UV_SHORT_LINK_KEY, fullShortUrl) + each);
                            // 标记是否是第一次访问
                            uvFirstFlag.set(uvadded != 0L);
                        }, addResponseCookieTask); // 如果不存在用户唯一标识，则执行添加任务
            } else {
                // 如果请求中没有Cookie，直接执行添加任务
                addResponseCookieTask.run();
            }
            // 获取IP地址
            String remoteAddr = LinkUtil.getActualIp(httpServletRequest);
            Long remoteAdded = stringRedisTemplate.opsForHyperLogLog().add(String.format(STATS_UV_SHORT_LINK_KEY, fullShortUrl) + remoteAddr);
            boolean uipFirstFlag = remoteAdded > 0L;
            // 如果没有提供gid，则从数据库中查询
            if (StrUtil.isBlank(gid)) {
                ShortLinkGotoDO shortLinkGotoDO = new LambdaQueryChainWrapper<>(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl)
                        .one();
                gid = shortLinkGotoDO.getGid();
            }
            // 构建链接访问统计对象，并插入数据库
            LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .gid(gid)
                    // 设置统计时间相关的字段
                    .hour(DateUtil.hour(new Date(), true))
                    .date(DateUtil.date())
                    .weekday(DateUtil.dayOfWeek(new Date()) - 1)
                    .pv(1) // 访问量为1
                    .uv(uvFirstFlag.get() ? 1 : 0)
                    .uip(uipFirstFlag ? 1 : 0)
                    .build();
            linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);
            // 获取IP地址对应的地理位置信息
            HashMap<String, Object> localeParamMap = new HashMap<>();
            localeParamMap.put("key", amapKey);
            localeParamMap.put("ip", remoteAddr);
            String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
            JSONObject localeResultObj = JSON.parseObject(localeResultStr);
            String infocode = localeResultObj.getString("infocode");
            String actualProvince = null;
            String actualCity = null;
            if (StrUtil.isNotBlank(infocode) && StrUtil.equals(infocode, "10000")) {
                String province = localeResultObj.getString("province");
                boolean unknownFlag = StrUtil.equals(province, "[]");
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .country("中国")
                        .province(actualProvince = unknownFlag ? "未知" : province)
                        .city(actualCity = unknownFlag ? "未知" : localeResultObj.getString("city"))
                        .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(DateUtil.date())
                        .build();
                linkLocaleStatsMapper.shortLinkLocaleStats(linkLocaleStatsDO);
            }
            // 操作系统类型统计
            String os = LinkUtil.getOs(httpServletRequest);
            if (StrUtil.isNotBlank(os)) {
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(DateUtil.date())
                        .os(os)
                        .build();
                linkOsStatsMapper.shortLinkOsStats(linkOsStatsDO);
            }
            // 浏览器类型统计
            String browser = LinkUtil.getBrowser(httpServletRequest);
            if (StrUtil.isNotBlank(browser)) {
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .cnt(1)
                        .fullShortUrl(fullShortUrl)
                        .gid(gid)
                        .date(DateUtil.date())
                        .browser(browser)
                        .build();
                linkBrowserStatsMapper.shortLinkBrowserStats(linkBrowserStatsDO);
            }
            // 记录访问设备监控数据
            String device = LinkUtil.getDevice(httpServletRequest);
            LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                    .device(device)
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);
            // 访问网络类型统计
            String network = LinkUtil.getNetwork(httpServletRequest);
            LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                    .network(network)
                    .cnt(1)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);
            // 用户高频访问日志
            LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                    .fullShortUrl(fullShortUrl)
                    .user(uv.get())
                    .ip(remoteAddr)
                    .network(network)
                    .device(device)
                    .locale(StrUtil.join("-", "中国", actualProvince, actualCity))
                    .browser(browser)
                    .os(os)
                    .gid(gid)
                    .build();
            linkAccessLogsMapper.insert(linkAccessLogsDO);
            baseMapper.incrementStats(gid, fullShortUrl, 1, uvFirstFlag.get() ? 1 : 0, uipFirstFlag ? 1 : 0);
            LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                    .todayPv(1)
                    .todayUv(uvFirstFlag.get() ? 1 : 0)
                    .todayUip(uipFirstFlag ? 1 : 0)
                    .gid(gid)
                    .fullShortUrl(fullShortUrl)
                    .date(new Date())
                    .build();
            linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
        } catch (Throwable ex) {
            // 记录统计过程中的异常
            log.error("短链接访问量统计异常", ex);
        }
    }


    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {
        // 获取短链接
        String shortLinkSuffix = generateSuffix(shortLinkCreateReqDTO);
        // 设置完整短链接
        String fullShortUrl = createShortLinkDefaultDomain + "/" + shortLinkSuffix;
        //构建短链接插入对象
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(shortLinkCreateReqDTO.getOriginUrl())
                .gid(shortLinkCreateReqDTO.getGid())
                .createdType(shortLinkCreateReqDTO.getCreatedType())
                .validDateType(shortLinkCreateReqDTO.getValidDateType())
                .validDate(shortLinkCreateReqDTO.getValidDate())
                .describe(shortLinkCreateReqDTO.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(shortLinkCreateReqDTO.getOriginUrl()))
                .build();
        //构建短链接跳转对象
        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .gid(shortLinkCreateReqDTO.getGid())
                .fullShortUrl(fullShortUrl)
                .build();
        try {
            save(shortLinkDO);
            shortLinkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException ex) {
            // TODO 已经误判了的短链接如何处理 （实际不会生成）
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(shortLinkDO)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
            ShortLinkDO hasShortLinkDO = getOne(queryWrapper);
            if (BeanUtil.isNotEmpty(hasShortLinkDO)) {
                log.warn("短链接:{}重复入库", fullShortUrl);
                throw new ServiceException("短链接生成重复");
            }
        }
        // 缓存雪崩：热点缓存预热
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                shortLinkCreateReqDTO.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(shortLinkCreateReqDTO.getValidDate()),
                TimeUnit.MILLISECONDS
        );
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl(shortLinkDO.getFullShortUrl())
                .originUrl(shortLinkCreateReqDTO.getOriginUrl())
                .gid(shortLinkCreateReqDTO.getGid())
                .build();
    }

    /**
     * 批量创建短链接
     * @param requestParam
     * @return
     */
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try {
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())
                        .originUrl(shortLink.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO shortLinkUpdateReqDTO) {
        ShortLinkDO hasShortLinkDO = lambdaQuery()
                //TODO 这里有bug
                .eq(ShortLinkDO::getGid, shortLinkUpdateReqDTO.getGid())
                .eq(ShortLinkDO::getEnableStatus, 0)
                .eq(ShortLinkDO::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                .one();
        //ShortLinkDO hasShortLinkDO = getOne(queryChainWrapper);
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接不存在");
        }
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(hasShortLinkDO.getDomain())
                .shortUri(hasShortLinkDO.getShortUri())
                .clickNum(hasShortLinkDO.getClickNum())
                .favicon(hasShortLinkDO.getFavicon())
                .createdType(hasShortLinkDO.getCreatedType())
                .gid(shortLinkUpdateReqDTO.getGid())
                .originUrl(shortLinkUpdateReqDTO.getOriginUrl())
                .describe(shortLinkUpdateReqDTO.getDescribe())
                .validDateType(shortLinkUpdateReqDTO.getValidDateType())
                .validDate(shortLinkUpdateReqDTO.getValidDate())
                .build();
        if (Objects.equals(hasShortLinkDO.getGid(), shortLinkUpdateReqDTO.getGid())) {
            lambdaUpdate()
                    .eq(ShortLinkDO::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, shortLinkUpdateReqDTO.getGid())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .set(Objects.equals(shortLinkUpdateReqDTO.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null)
                    .update(shortLinkDO);
        } else {
            lambdaUpdate()
                    .eq(ShortLinkDO::getFullShortUrl, shortLinkUpdateReqDTO.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .remove();
            save(shortLinkDO);
        }
        //改为过期，删除跳转缓存
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), shortLinkUpdateReqDTO.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(), shortLinkUpdateReqDTO.getValidDate())) {
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, shortLinkUpdateReqDTO.getFullShortUrl()));
            //当改为有效期时 需删除空缓存
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().isBefore(LocalDateTime.now())) {
                if (Objects.equals(shortLinkUpdateReqDTO.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) || shortLinkUpdateReqDTO.getValidDate().isAfter(LocalDateTime.now())) {
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, shortLinkUpdateReqDTO.getFullShortUrl()));
                }
            }
        }
    }

    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO shortLinkPageReqDTO) {
        /*LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, shortLinkPageReqDTO.getGid())
                .eq(ShortLinkDO::getEnableStatus, 0);
        IPage<ShortLinkDO> resultPage = this.page(shortLinkPageReqDTO, queryWrapper);*/
        //IPage<ShortLinkDO> resultPage = baseMapper.selectPage(shortLinkPageReqDTO, queryWrapper);
        //Page<ShortLinkDO> page = lambdaQuery()
        //        .eq(ShortLinkDO::getGid, shortLinkPageReqDTO.getGid())
        //        .eq(ShortLinkDO::getEnableStatus, 0)
        //        .orderByDesc(ShortLinkDO::getCreateTime)
        //        .page(Page.of(shortLinkPageReqDTO.getCurrent(), shortLinkPageReqDTO.getSize()));
        //return resultPage.convert(each->BeanUtil.toBean(each, ShortLinkPageRespDTO.class));
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(shortLinkPageReqDTO);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }

    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> gids) {
        /*Wrappers.query(new ShortLinkDO())
                .select("gid,count(*)")
                .in("gid", gids)
                .eq("enable_status", 0)
                .groupBy("gid");
        Wrappers.query(ShortLinkDO.class)
                .select("gid,count(*)")
                .in("gid", gids)
                .eq("enable_status", 0)
                .groupBy("gid");*/
        QueryWrapper<ShortLinkDO> shortLinkDOQueryWrapper = new QueryWrapper<ShortLinkDO>()
                .select("gid as gid,count(*) as shortLinkCount")
                .in("gid", gids)
                .eq("enable_status", 0)
                .groupBy("gid");
        List<Map<String, Object>> maps = listMaps(shortLinkDOQueryWrapper);

        return BeanUtil.copyToList(maps, ShortLinkGroupCountQueryRespDTO.class);
    }

    public String generateSuffix(ShortLinkCreateReqDTO shortLinkCreateReqDTO) {
        // 自定义生成计数器
        int customGenerateCount = 0;
        String shortUri;
        while (true) {
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            // 生成短链接
            shortUri = HashUtil.hashToBase62(shortLinkCreateReqDTO.getOriginUrl() + System.currentTimeMillis());
            // 如果完整短链接不在布隆过滤器里 添加进布隆过滤器返回
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shortUri)) {
                shortUriCreateCachePenetrationBloomFilter.add(createShortLinkDefaultDomain + "/" + shortUri);
                break;
            }
            customGenerateCount++;
        }
        return shortUri;
    }

    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}
